/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.itest.springboot.command;

import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;

import org.apache.camel.itest.springboot.Command;
import org.apache.camel.itest.springboot.ITestConfig;

import org.junit.jupiter.api.Assertions;
import org.junit.platform.engine.FilterResult;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;

import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
import static org.junit.platform.engine.discovery.ClassNameFilter.*;

import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import org.junit.platform.launcher.listeners.TestExecutionSummary.Failure;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * A command that executes all unit tests contained in the module.
 */
@Component("unittest")
public class UnitTestCommand extends AbstractTestCommand implements Command {

    Logger logger = LoggerFactory.getLogger(UnitTestCommand.class);

    @Override
    public UnitTestResult executeTest(final ITestConfig config, String component) throws Exception {

        logger.info("Spring-Boot test configuration {}", config);

        logger.info("Scanning the classpath for test classes");
        LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
                .selectors(selectDirectory(config.getModuleBasePath()), selectPackage(config.getUnitTestBasePackage()))
                .filters(includeClassNamePatterns(config.getUnitTestInclusionPattern()),
                        excludeClassNamePatterns(config.getUnitTestExclusionPattern()), new IsAdmissableFilter(),
                        new HasSpringbootAnnotationFilter())
                .build();

        // Add classes to the Request
        Launcher launcher = LauncherFactory.create();
        SummaryGeneratingListener listener = new SummaryGeneratingListener();
        launcher.registerTestExecutionListeners(listener);
        if (!config.getJmxDisabledNames().isEmpty()) {
            launcher.registerTestExecutionListeners(new TestExecutionListener() {
                @Override
                public void executionStarted(TestIdentifier testId) {
                    if (testId.isTest()) {
                        try {
                            disableJmx(config.getJmxDisabledNames());
                        } catch (Exception e) {
                            logger.error("Exception disabling JMX for test " + testId.getDisplayName(), e);
                        }
                    }
                }
            });
        }
        TestPlan testPlan = launcher.discover(request);
        long nbTests = testPlan.countTestIdentifiers((t) -> {
            return t.isTest();
        });
        if (nbTests > 0) {
            logger.info("Found {} JUnit5 tests", nbTests);
            launcher.execute(testPlan);

            TestExecutionSummary result = listener.getSummary();
            boolean testSucceeded = result.getTestsFailedCount() == 0;

            logger.info(config.getModuleName() + " unit tests. " + "Success: " + testSucceeded + " - Test Run: "
                    + result.getTestsStartedCount() + " - Failures: " + result.getTestsFailedCount()
                    + " - Skipped Tests: " + result.getTestsSkippedCount());

            StringBuilder failureString = new StringBuilder();
            Throwable failureException = null;
            for (Failure f : result.getFailures()) {
                failureString
                        .append(f.getTestIdentifier().getDisplayName() + " - " + f.getException().getMessage() + "\n");
                logger.warn("Failed test description: {}", f.getTestIdentifier());
                logger.warn("Message: {}", f.getException().getMessage());
                if (f.getException() != null) {
                    logger.error("Exception thrown from test", f.getException());
                    failureException = f.getException();
                }
            }

            if (!testSucceeded) {
                if (failureException != null) {
                    Assertions.fail(
                            "Some unit tests failed (" + result.getTestsFailedCount() + "/"
                                    + result.getTestsStartedCount() + ") : " + failureString.toString(),
                            failureException);
                } else {
                    Assertions.fail("Some unit tests failed (" + result.getTestsFailedCount() + "/"
                            + result.getTestsStartedCount() + ") : " + failureString.toString());
                }
            }

            if (result.getTestsStartedCount() == 0 && config.getUnitTestsExpectedNumber() == null) {
                Assertions.fail("No tests have been found");
            }

            Integer expectedTests = config.getUnitTestsExpectedNumber();
            if (expectedTests != null && expectedTests != result.getTestsStartedCount()) {
                Assertions.fail(
                        "Wrong number of tests: expected " + expectedTests + " found " + result.getTestsStartedCount());
            }

            return new UnitTestResult(result);
        } else {
            logger.warn("No JUnit5 tests found for component {}", component);
            return null;
        }
    }

    private void disableJmx(Set<String> disabledJmx) throws Exception {
        logger.info("Disabling JMX names: {}", disabledJmx);
        for (MBeanServer server : getMBeanServers()) {
            for (String jmxName : disabledJmx) {
                logger.info("Disabling JMX query {}", jmxName);

                ObjectName oName = new ObjectName(jmxName);
                Set<ObjectName> names = new HashSet<>(server.queryNames(oName, null));
                for (ObjectName name : names) {
                    logger.info("Disabled JMX name {}", name);
                    server.unregisterMBean(name);
                }
            }
        }
    }

    private List<MBeanServer> getMBeanServers() {
        List<MBeanServer> servers = MBeanServerFactory.findMBeanServer(null);
        if (servers == null) {
            servers = Collections.emptyList();
        }
        return servers;
    }

    private class HasSpringbootAnnotationFilter implements PostDiscoveryFilter {

        @Override
        public FilterResult apply(TestDescriptor testDescriptor) {
            Optional<TestSource> source = testDescriptor.getSource();
            Class<?> testClass = null;
            if (source.isPresent() && source.get() instanceof MethodSource) {
                if (source.get() instanceof MethodSource) {
                    testClass = ((MethodSource) source.get()).getJavaClass();
                } else if (source.get() instanceof ClassSource) {
                    testClass = ((ClassSource) source.get()).getJavaClass();
                }
            }
            if (testClass != null) {
                logger.debug("Checking class " + testClass.getName());
                for (Annotation annotation : testClass.getAnnotations()) {
                    if (annotation.toString().contains("org.apache.camel.test.spring.junit5.CamelSpringBootTest")
                            || annotation.toString().contains("org.springframework.boot.test.context.SpringBootTest")) {
                        return FilterResult.excluded("Not admissable");
                    }
                }
            }

            return FilterResult.included("");
        }
    }

    private class IsAdmissableFilter implements PostDiscoveryFilter {

        @Override
        public FilterResult apply(TestDescriptor testDescriptor) {
            Optional<TestSource> source = testDescriptor.getSource();
            if (source.isPresent() && source.get() instanceof ClassSource) {
                Class<?> testClass = ((ClassSource) source.get()).getJavaClass();
                logger.debug("Checking class " + testClass.getName());
                if (!isAdmissible(testClass)) {
                    return FilterResult.excluded("Not admissable");
                } else {
                    return FilterResult.included("Admissable");
                }
            } else {
                return FilterResult.included("");
            }
        }
    }

    private boolean isAdmissible(Class<?> testClass) {

        if (testClass.getPackage().getName().startsWith("org.apache.camel.itest.springboot")) {
            // no tests from the integration test suite
            return false;
        }

        URL location = testClass.getResource("/" + testClass.getName().replace(".", "/") + ".class");
        if (location != null) {
            int firstLevel = location.toString().indexOf("!/");
            int lastLevel = location.toString().lastIndexOf("!/");
            if (firstLevel >= 0 && lastLevel >= 0 && firstLevel != lastLevel) {
                // test class is in a nested jar, skipping
                return false;
            }
        }

        return true;
    }
}
